Eine umfassende Anleitung zum 'Never'-Typ. Erfahren Sie, wie Sie erschöpfende Überprüfungen für robusten, fehlerfreien Code nutzen und seine Beziehung zur traditionellen Fehlerbehandlung verstehen.
Der Never-Typ: Vom Umstellen von Laufzeitfehlern auf Kompilierzeit-Garantien
In der Welt der Softwareentwicklung verbringen wir viel Zeit und Mühe damit, Fehler zu verhindern, zu finden und zu beheben. Einige der heimtückischsten Fehler sind diejenigen, die stillschweigend auftreten. Sie lassen die Anwendung nicht sofort abstürzen; stattdessen verstecken sie sich in unbehandelten Randfällen und warten auf eine bestimmte Datenmenge oder eine Benutzeraktion, um ein falsches Verhalten auszulösen. Eine häufige Ursache für solche Fehler ist ein einfaches Versehen: Ein Entwickler fügt einer Reihe von Optionen eine neue Option hinzu, vergisst aber, alle Stellen im Code zu aktualisieren, die sie verarbeiten müssen.
Stellen Sie sich eine `switch`-Anweisung vor, die verschiedene Arten von Benutzerbenachrichtigungen verarbeitet. Was passiert, wenn ein neuer Benachrichtigungstyp, z. B. 'POLL_RESULT', hinzugefügt wird und wir vergessen, einen entsprechenden `case`-Block in unserer Benachrichtigungs-Rendering-Funktion hinzuzufügen? In vielen Sprachen fällt der Code einfach durch, tut nichts und scheitert stillschweigend. Der Benutzer sieht das Umfrageergebnis nie, und wir entdecken den Fehler möglicherweise erst nach Wochen.
Was wäre, wenn der Compiler dies verhindern könnte? Was wäre, wenn unsere eigenen Werkzeuge uns zwingen könnten, jede Möglichkeit zu berücksichtigen und einen potenziellen Logikfehler zur Laufzeit in einen Typfehler zur Kompilierzeit zu verwandeln? Genau das ist die Leistungsfähigkeit des 'Never'-Typs, eines Konzepts, das in modernen, statisch typisierten Sprachen zu finden ist. Es ist ein Mechanismus zur Durchsetzung der erschöpfenden Überprüfung, der eine robuste Kompilierzeitgarantie dafür bietet, dass alle Fälle behandelt werden. Dieser Artikel untersucht den `never`-Typ, stellt seine Rolle der traditionellen Fehlerbehandlung gegenüber und demonstriert, wie Sie ihn verwenden können, um widerstandsfähigere und wartbarere Softwaresysteme zu erstellen.
Was genau ist der 'Never'-Typ?
Auf den ersten Blick mag der `never`-Typ esoterisch oder rein akademisch erscheinen. Seine praktischen Auswirkungen sind jedoch tiefgreifend. Um ihn zu verstehen, müssen wir seine beiden Haupteigenschaften erfassen.
Ein Typ für das Unmögliche
Der `never`-Typ stellt einen Wert dar, der niemals vorkommen kann. Es ist ein Typ, der keine möglichen Werte enthält. Das klingt abstrakt, aber es wird verwendet, um zwei Hauptszenarien zu bezeichnen:
- Eine Funktion, die nie zurückkehrt: Das bedeutet nicht eine Funktion, die nichts zurückgibt (das ist `void`). Es bedeutet eine Funktion, die nie ihren Endpunkt erreicht. Sie wirft möglicherweise einen Fehler, oder sie tritt möglicherweise in eine Endlosschleife ein. Der springende Punkt ist, dass der normale Ausführungsablauf dauerhaft unterbrochen wird.
- Eine Variable in einem unmöglichen Zustand: Durch logische Deduktion (ein Prozess namens Typenverengung) kann der Compiler feststellen, dass eine Variable innerhalb eines bestimmten Codeblocks möglicherweise keinen Wert halten kann. In dieser Situation ist der Typ der Variable effektiv `never`.
In der Typentheorie ist `never` als der Bottom-Typ (oft mit ⊥ bezeichnet) bekannt. Der Bottom-Typ zu sein, bedeutet, dass er ein Subtyp von jedem anderen Typ ist. Das macht Sinn: Da ein Wert vom Typ `never` nie existieren kann, kann er einer Variable vom Typ `string`, `number` oder `User` zugewiesen werden, ohne die Typsicherheit zu verletzen, da diese Codezeile nachweislich unerreichbar ist.
Entscheidender Unterschied: `never` vs. `void`
Ein häufiger Punkt der Verwirrung ist der Unterschied zwischen `never` und `void`. Der Unterschied ist entscheidend:
void: Repräsentiert das Fehlen eines verwendbaren Rückgabewerts. Die Funktion wird bis zum Abschluss ausgeführt und gibt zurück, aber ihr Rückgabewert ist nicht zur Verwendung bestimmt. Denken Sie an eine Funktion, die nur in die Konsole protokolliert.never: Repräsentiert die Unmöglichkeit der Rückgabe. Die Funktion garantiert, dass sie ihren Ausführungspfad nicht normal abschließt.
Sehen wir uns ein TypeScript-Beispiel an:
// Diese Funktion gibt 'void' zurück. Sie wird erfolgreich abgeschlossen.
function logMessage(message: string): void {
console.log(message);
// Gibt implizit 'undefined' zurück
}
// Diese Funktion gibt 'never' zurück. Sie wird nie abgeschlossen.
function throwError(message: string): never {
throw new Error(message);
}
// Diese Funktion gibt aufgrund einer Endlosschleife ebenfalls 'never' zurück.
function processTasks(): never {
while (true) {
// ... eine Aufgabe aus einer Warteschlange verarbeiten
}
}
Das Verständnis dieses Unterschieds ist der erste Schritt, um die praktische Leistungsfähigkeit von `never` zu erschließen.
Der Kernanwendungsfall: Erschöpfende Überprüfung
Die wirkungsvollste Anwendung des `never`-Typs ist die Durchsetzung erschöpfender Überprüfungen zur Kompilierzeit. Er ermöglicht es uns, ein Sicherheitsnetz zu erstellen, das sicherstellt, dass wir jede Variante eines bestimmten Datentyps behandelt haben.
Das Problem: Die fragile `switch`-Anweisung
Modellieren wir eine Reihe von geometrischen Formen mit einer diskriminierten Union. Dies ist ein leistungsstarkes Muster, bei dem Sie eine gemeinsame Eigenschaft (den 'Diskriminator', wie z. B. `kind`) haben, die Ihnen sagt, mit welcher Variante des Typs Sie es zu tun haben.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; sideLength: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
}
// Was passiert, wenn wir eine Form erhalten, die wir nicht erkennen?
// Diese Funktion würde implizit 'undefined' zurückgeben, wahrscheinlich ein Fehler!
}
Dieser Code funktioniert vorerst. Aber was passiert, wenn sich unsere Anwendung weiterentwickelt? Ein Kollege fügt eine neue Form hinzu:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; sideLength: number }
| { kind: 'rectangle'; width: number; height: number }; // Neue Form hinzugefügt!
Die Funktion `getArea` ist jetzt unvollständig. Wenn sie ein `rectangle` empfängt, hat die `switch`-Anweisung keinen übereinstimmenden Fall, die Funktion wird abgeschlossen, und in JavaScript/TypeScript gibt sie `undefined` zurück. Der aufrufende Code erwartete eine `number`, erhält aber `undefined`, was zu einem `NaN`-Fehler oder anderen subtilen Fehlern weit nachgelagert führt. Der Compiler hat uns keine Warnung gegeben.
Die Lösung: Der `never`-Typ als Schutz
Wir können dies beheben, indem wir den `never`-Typ im `default`-Fall unserer `switch`-Anweisung verwenden. Diese einfache Ergänzung verwandelt den Compiler in unseren aufmerksamen Partner.
function getAreaWithExhaustiveCheck(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
// Was ist mit 'rectangle'? Wir haben es vergessen.
default:
// Hier geschieht die Magie.
const _exhaustiveCheck: never = shape;
// Die obige Zeile verursacht jetzt einen Kompilierzeitfehler!
// Type 'Rectangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
Lassen Sie uns aufschlüsseln, warum dies funktioniert:
- Typenverengung: Innerhalb jedes `case`-Blocks ist der Compiler von TypeScript intelligent genug, um den Typ der Variable `shape` zu verengen. In `case 'circle'` weiß der Compiler, dass `shape` `{ kind: 'circle'; radius: number }` ist.
- Der `default`-Block: Wenn der Code den `default`-Block erreicht, leitet der Compiler ab, welche Typen `shape` möglicherweise sein könnte. Er subtrahiert alle behandelten Fälle von der ursprünglichen `Shape`-Union.
- Das Fehlerszenario: In unserem aktualisierten Beispiel haben wir `'circle'` und `'square'` behandelt. Daher weiß der Compiler innerhalb des `default`-Blocks, dass `shape` `{ kind: 'rectangle'; ... }` sein muss. Unser Code versucht dann, dieses `rectangle`-Objekt der Variable `_exhaustiveCheck` zuzuweisen, die den Typ `never` hat. Diese Zuweisung schlägt mit einem klaren Typfehler fehl: `Type 'Rectangle' is not assignable to type 'never'`. Der Fehler wird erkannt, bevor der Code jemals ausgeführt wird!
- Das Erfolgsszenario: Wenn wir den `case` für `'rectangle'` hinzufügen, hat der Compiler im `default`-Block alle Möglichkeiten ausgeschöpft. Der Typ von `shape` wird auf `never` verengt (es kann kein Kreis, Quadrat oder Rechteck sein, also ist es ein unmöglicher Typ). Das Zuweisen eines Werts vom Typ `never` zu einer Variable vom Typ `never` ist absolut gültig. Der Code wird ohne Fehler kompiliert.
Dieses Muster, das oft als "Erschöpfungstrick" bezeichnet wird, bevollmächtigt den Compiler effektiv, die Vollständigkeit zu erzwingen. Es verwandelt eine fragile Laufzeitkonvention in eine grundsolide Kompilierzeitgarantie.
Erschöpfende Überprüfung vs. traditionelle Fehlerbehandlung
Man ist versucht, die erschöpfende Überprüfung als Ersatz für die Fehlerbehandlung zu betrachten, aber das ist ein Missverständnis. Sie sind komplementäre Werkzeuge, die zur Lösung verschiedener Klassen von Problemen entwickelt wurden. Der Hauptunterschied liegt darin, was sie behandeln sollen: vorhersehbare, bekannte Zustände versus unvorhersehbare, außergewöhnliche Ereignisse.
Definition der Konzepte
-
Fehlerbehandlung ist eine Laufzeit-Strategie zur Verwaltung außergewöhnlicher und unvorhersehbarer Situationen, die oft außerhalb der Kontrolle des Programms liegen. Sie befasst sich mit Fehlern, die während der Ausführung auftreten können und auftreten.
- Beispiele: Fehler bei Netzwerkanfragen, eine Datei wird auf der Festplatte nicht gefunden, ungültige Benutzereingabe, Zeitüberschreitung der Datenbankverbindung.
- Werkzeuge: `try...catch`-Blöcke, `Promise.reject()`, Rückgabe von Fehlercodes oder `null`, `Result`-Typen (wie in Sprachen wie Rust zu sehen).
-
Erschöpfende Überprüfung ist eine Kompilierzeit-Strategie, um sicherzustellen, dass alle bekannten, gültigen logischen Pfade oder Datenzustände explizit innerhalb der Logik des Programms behandelt werden. Es geht darum, sicherzustellen, dass Ihr Code vollständig ist.
- Beispiele: Behandlung aller Varianten eines Enums, Verarbeitung aller Typen in einer diskriminierten Union, Verwaltung aller Zustände einer endlichen Zustandsmaschine.
- Werkzeuge: Der `never`-Typ, sprachgestützte `switch`- oder `match`-Erschöpfung (wie in Swift und Rust zu sehen).
Das Leitprinzip: Bekannte vs. Unbekannte
Eine einfache Möglichkeit, zu entscheiden, welchen Ansatz Sie verwenden sollen, besteht darin, sich nach der Art des Problems zu fragen:
- Ist dies eine Reihe von Möglichkeiten, die ich innerhalb meiner Codebasis definiert habe und kontrolliere? Verwenden Sie die erschöpfende Überprüfung. Dies sind Ihre "Bekannten". Ihre `Shape`-Union ist ein perfektes Beispiel; Sie definieren alle möglichen Formen.
- Ist dies ein Ereignis, das von einem externen System, einem Benutzer oder der Umgebung stammt, bei dem ein Fehler möglich ist und die genaue Eingabe unvorhersehbar ist? Verwenden Sie die Fehlerbehandlung. Dies sind Ihre "Unbekannten". Sie können das Typsystem nicht verwenden, um zu beweisen, dass ein Netzwerk immer verfügbar sein wird.
Szenarioanalyse: Wann was verwenden
Szenario 1: Parsen der API-Antwort (Fehlerbehandlung)
Stellen Sie sich vor, Sie rufen Benutzerdaten von einer API eines Drittanbieters ab. Die API-Dokumentation besagt, dass sie ein JSON-Objekt mit einem Feld `status` zurückgibt. Sie können dies zur Kompilierzeit nicht vertrauen. Das Netzwerk könnte ausgefallen sein, die API könnte veraltet sein und einen 500-Fehler zurückgeben oder eine fehlerhafte JSON-Zeichenfolge zurückgeben. Dies ist der Bereich der Fehlerbehandlung.
async function fetchUser(userId: string): Promise<User> {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
// HTTP-Fehler behandeln (z. B. 404, 500)
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
// Hier würden Sie auch eine Laufzeitvalidierung der Datenstruktur hinzufügen
return data as User;
} catch (error) {
// Netzwerkfehler, JSON-Parsing-Fehler usw. behandeln
console.error("Failed to fetch user:", error);
throw error; // Erneut auslösen oder anmutig behandeln
}
}
Die Verwendung von `never` hier wäre unangemessen, da die Möglichkeiten für Fehler unendlich und extern zu unserem Typsystem sind.
Szenario 2: Rendern eines UI-Komponentenstatus (erschöpfende Überprüfung)
Nehmen wir nun an, dass sich Ihre UI-Komponente in einem von mehreren gut definierten Zuständen befinden kann. Sie steuern diese Zustände vollständig innerhalb Ihres Anwendungscodes. Dies ist ein perfekter Kandidat für eine diskriminierte Union und eine erschöpfende Überprüfung.
type ComponentState =
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; message: string };
function renderComponent(state: ComponentState): string { // Gibt eine HTML-Zeichenfolge zurück
switch (state.status) {
case 'loading':
return `<div>Loading...</div>`;
case 'success':
return `<ul>${state.data.map(item => `<li>${item}</li>`).join('')}</ul>`;
case 'error':
return `<div class="error">Error: ${state.message}</div>`;
default:
// Wenn wir später einen 'submitting'-Status hinzufügen, schützt uns diese Zeile!
const _exhaustiveCheck: never = state;
throw new Error(`Unhandled state: ${_exhaustiveCheck}`);
}
}
Wenn ein Entwickler einen neuen Status, `{ status: 'idle' }`, hinzufügt, kennzeichnet der Compiler `renderComponent` sofort als unvollständig, wodurch ein UI-Fehler verhindert wird, bei dem die Komponente als leerer Raum gerendert wird.
Die Synergie: Kombination beider Ansätze für robuste Systeme
Die widerstandsfähigsten Systeme wählen nicht das eine vor dem anderen; sie verwenden beide im Einklang. Die Fehlerbehandlung verwaltet die chaotische äußere Welt, während die erschöpfende Überprüfung sicherstellt, dass die interne Logik solide und vollständig ist. Die Ausgabe einer Fehlerbehandlungsgrenze wird oft zur Eingabe für ein System, das sich auf die erschöpfende Überprüfung verlässt.
Verfeinern wir unser API-Abrufbeispiel. Die Funktion kann unvorhersehbare Netzwerkfehler behandeln, aber sobald sie auf kontrollierte Weise erfolgreich ist oder fehlschlägt, gibt sie ein vorhersehbares, gut typisiertes Ergebnis zurück, das der Rest unserer Anwendung mit Zuversicht verarbeiten kann.
// 1. Definieren Sie ein vorhersehbares, gut typisiertes Ergebnis für unsere interne Logik.
type FetchResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// 2. Die Funktion verwendet jetzt die Fehlerbehandlung, um ein Ergebnis zu erzeugen, das erschöpfend überprüft werden kann.
async function fetchUserData(userId: string): Promise<FetchResult<User>> {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
const data = await response.json();
// Fügen Sie hier eine Laufzeitvalidierung hinzu (z. B. mit Zod oder io-ts)
return { status: 'success', data: data as User };
} catch (error) {
// Wir fangen ALLE potenziellen Fehler ab und verpacken sie in unsere bekannte Struktur.
return { status: 'error', error: error instanceof Error ? error : new Error('An unknown error occurred') };
}
}
// 3. Der aufrufende Code kann jetzt die erschöpfende Überprüfung für eine saubere, sichere Logik verwenden.
async function displayUser(userId: string) {
const result = await fetchUserData(userId);
switch (result.status) {
case 'success':
console.log(`User name: ${result.data.name}`);
break;
case 'error':
console.error(`Failed to display user: ${result.error.message}`);
break;
default:
const _exhaustiveCheck: never = result;
// Dies stellt sicher, dass dieser Codeblock nicht kompiliert, wenn wir FetchResult einen 'loading'-Status hinzufügen,
// bis wir ihn verarbeiten.
return _exhaustiveCheck;
}
}
Dieses kombinierte Muster ist unglaublich leistungsstark. Die Funktion `fetchUserData` fungiert als Grenze und übersetzt die unvorhersehbare Welt der Netzwerkanforderungen in eine vorhersehbare, diskriminierte Union. Der Rest der Anwendung kann dann auf dieser sauberen Datenstruktur mit dem vollen Sicherheitsnetz der Kompilierzeit-Erschöpfungsprüfungen arbeiten.
Eine globale Perspektive: `never` in anderen Sprachen
Das Konzept eines Bottom-Typs und der Erschöpfung zur Kompilierzeit ist nicht nur für TypeScript einzigartig. Es ist ein Markenzeichen vieler moderner, auf Sicherheit ausgerichteter Sprachen. Zu sehen, wie es anderswo implementiert wird, verstärkt seine grundlegende Bedeutung in der Softwareentwicklung.
- Rust: Rust hat einen `!`-Typ, den sogenannten "Never-Typ". Es ist der Rückgabetyp von Funktionen, die "abweichen", z. B. das Makro `panic!()`, das den aktuellen Ausführungsthread beendet. Der leistungsstarke `match`-Ausdruck von Rust (seine Version von `switch`) erzwingt standardmäßig die Erschöpfung. Wenn Sie auf ein `enum` `match` abgleichen und nicht alle Varianten abdecken, wird der Code nicht kompiliert. Sie benötigen nicht den manuellen `never`-Trick, da die Sprache diese Sicherheit sofort bietet.
- Swift: Swift hat ein leeres Enum namens `Never`. Es wird verwendet, um anzugeben, dass eine Funktion oder Methode niemals zurückkehrt, entweder durch Auslösen eines Fehlers oder durch Nichtbeenden. Wie Rust müssen die `switch`-Anweisungen von Swift standardmäßig erschöpfend sein, was zur Kompilierzeit Sicherheit bietet, wenn mit Enums gearbeitet wird.
- Kotlin: Kotlin hat den Typ `Nothing`, der der Bottom-Typ seines Typsystems ist. Es wird verwendet, um anzugeben, dass eine Funktion nie zurückkehrt, z. B. die `TODO()`-Funktion der Standardbibliothek, die immer einen Fehler auslöst. Der `when`-Ausdruck von Kotlin (das Äquivalent zu `switch`) kann auch für erschöpfende Überprüfungen verwendet werden, und der Compiler gibt eine Warnung oder einen Fehler aus, wenn er nicht erschöpfend ist, wenn er als Ausdruck verwendet wird.
- Python (mit Typ-Hinweisen): Das Modul `typing` von Python enthält `NoReturn`, mit dem Funktionen annotiert werden können, die nie zurückkehren. Während das Typsystem von Python allmählich ist und nicht so streng wie das von Rust oder Swift, bieten diese Anmerkungen wertvolle Informationen für statische Analysetools wie Mypy, die dann gründlichere Überprüfungen durchführen können.
Der gemeinsame Nenner in diesen verschiedenen Ökosystemen ist die Erkenntnis, dass das Unmöglichmachen von unvertretbaren Zuständen auf Typebene eine leistungsstarke Möglichkeit ist, ganze Klassen von Fehlern zu eliminieren.
Umfassende Erkenntnisse und Best Practices
Um dieses leistungsstarke Konzept in Ihre tägliche Arbeit zu integrieren, sollten Sie die folgenden Vorgehensweisen in Betracht ziehen:
- Umfassen Sie diskriminierte Unions: Modellieren Sie Ihre Daten aktiv mit diskriminierten Unions (auch als getaggte Unions oder Summentypen bezeichnet), wann immer Sie einen Typ haben, der eine von mehreren verschiedenen Varianten sein kann. Dies ist die Grundlage, auf der die erschöpfende Überprüfung aufgebaut ist. Modellieren Sie API-Ergebnisse, Komponentenstatus und Ereignisse auf diese Weise.
- Machen Sie illegale Zustände unvertretbar: Dies ist ein Kernsatz des typgesteuerten Designs. Wenn ein Benutzer nicht gleichzeitig Administrator und Gast sein kann, sollte Ihr Typsystem dies widerspiegeln. Verwenden Sie Unions (`A | B`) anstelle von mehreren optionalen booleschen Flags (`isAdmin?: boolean; isGuest?: boolean;`). Der `never`-Typ ist das ultimative Werkzeug, um zu beweisen, dass ein Zustand unvertretbar ist.
-
Erstellen Sie eine wiederverwendbare Hilfsfunktion: Der `default`-Fall kann mit einer einfachen Hilfsfunktion sauberer gestaltet werden. Dies liefert auch eine aussagekräftigere Fehlermeldung, wenn der Code jemals zur Laufzeit erreicht wird (was unmöglich sein sollte).
function assertNever(value: never): never { throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`); } // Verwendung: default: assertNever(shape); // Sauberer und bietet eine bessere Laufzeitfehlermeldung. - Hören Sie auf Ihren Compiler: Behandeln Sie einen Erschöpfungsfehler nicht als Ärgernis, sondern als Geschenk. Der Compiler fungiert als fleißiger, automatisierter Code-Rezensent, der einen logischen Fehler in Ihrem Programm gefunden hat. Bedanken Sie sich und beheben Sie den Code.
Schlussfolgerung: Der stille Hüter Ihrer Codebasis
Der `never`-Typ ist weit mehr als eine theoretische Kuriosität; Er ist ein pragmatisches und leistungsstarkes Werkzeug zum Erstellen robuster, selbstdokumentierender und wartbarer Software. Indem wir ihn für die erschöpfende Überprüfung nutzen, ändern wir grundlegend, wie wir an die Korrektheit herangehen. Wir verlagern die Last, die logische Vollständigkeit sicherzustellen, vom fehlbaren menschlichen Gedächtnis und Laufzeittests in die unfehlbare, automatisierte Welt der Kompilierzeit-Typenanalyse.
Während die traditionelle Fehlerbehandlung für die Verwaltung der unvorhersehbaren Natur externer Systeme unerlässlich bleibt, bietet die erschöpfende Überprüfung eine ergänzende Garantie für die interne, bekannte Logik unserer Anwendungen. Zusammen bilden sie eine mehrschichtige Verteidigung gegen Fehler und schaffen Systeme, die nicht nur weniger anfällig für Fehler sind, sondern auch einfacher zu verstehen und sicherer zu refaktorieren sind.
Wenn Sie das nächste Mal eine `switch`-Anweisung oder eine lange `if-else-if`-Kette über eine Reihe bekannter Möglichkeiten schreiben, halten Sie inne und fragen Sie sich: Kann der `never`-Typ als stiller Hüter für diesen Code dienen? Wenn Sie dies tun, schreiben Sie Code, der nicht nur heute korrekt ist, sondern auch vor den Versäumnissen von morgen geschützt ist.